<template>
  <view v-if="data.content" :style="{ width:addUnit(_props.maxWidth) }" class="text-ellipsis-wrap">
    <view v-show="data.show"
          :class="['text-ellipsis']" :style="[textStyle]">
      <text :style="[textStyle]"
            class="text-ellipsis-text">
        {{ data.isOpen ? _props.text : data.content }}
      </text>
      <text v-if="data.isActive"
            :style="[textStyle,{color: _props.suffixColor}]"
            class="text-ellipsis-active" @click="onClickAction">
        {{ data.suffixText }}
      </text>
    </view>
    <view id="hiddenText" :style="[textStyle]" class="hiddenText">
      <text :style="[textStyle]" class="hiddenText-text">
        {{ data.content }}
      </text>
    </view>
  </view>
</template>
<script setup>
import {reactive, nextTick, computed, onMounted, getCurrentInstance, watch} from 'vue';

const instance = getCurrentInstance();


/**
 * @description 文本省略号显示展开/收起
 * @param {String} text 文本内容
 * @param {Number|String} fontSize 文本行高(请传入字体大小的倍数)
 * @param {Number|String} lineHeight 文本行高
 * @param {String} color 文本颜色
 * @param {Number|String} maxLine 最大行数
 * @param {Number|String} maxWidth 最大宽度
 * @param {String} suffixOpen 展开文本
 * @param {String} suffixClose 收起文本
 * @param {String} suffixColor 展开/收起文本颜色
 */
const _props = defineProps({
  text: {
    type: String,
    required: true,
  },
  fontSize: {
    type: [Number, String],
    default: 30,
  },
  lineHeight: {
    type: [Number, String],
    default: 1.2,
    validator(val) {
      return !isNaN(val);
    },
  },
  color: {
    type: String,
    default: '#333',
  },
  maxLine: {
    type: [Number, String],
    default: 3,
    validator(val) {
      return !isNaN(val);
    },
  },
  maxWidth: {
    type: [Number, String],
    default: 500,
    validator(val) {
      return !isNaN(val);
    },
  },
  suffixColor: {
    type: String,
    default: '#999',
  },
  suffixOpen: {
    type: String,
    default: '展开',
  },
  suffixClose: {
    type: String,
    default: '收起',
  },
});


const data = reactive({
  show: true,
  content: '',
  isOpen: false,
  isActive: false,
  suffixText: '',
  dots: '...',
  scaleFactor: '',
});

// 监听 text 属性变化
watch(() => _props.text, (newVal) => {
  if (newVal) {
    data.content = newVal;
    data.suffixText = _props.suffixOpen;
    nextTick(() => {
      initEllipsis();
    });
  }
}, { immediate: true });

// 初始化省略号
async function initEllipsis() {
  try {
    let height = await queryRectProp();
    let maxHeight = parseFloat(_props.lineHeight) * parseFloat(_props.fontSize) * _props.maxLine / data.scaleFactor;
    if (maxHeight < parseInt(height)) {
      data.isActive = true;
      await calcEllipsisText(maxHeight);
    } else {
      data.isActive = false;
      data.content = _props.text;
    }
    data.show = true;
  } catch (error) {
    console.error('获取元素高度失败:', error);
    data.content = _props.text;
    data.show = true;
  }
}

onMounted(() => {
  uni.getSystemInfo({
    success: (res) => {
      data.scaleFactor = 750 / res.windowWidth;
      initEllipsis();
    }
  });
});

const textStyle = computed(() => {
  return {
    color: _props.color,
    lineHeight: _props.lineHeight,
    fontSize: addUnit(_props.fontSize),
    width: addUnit(_props.maxWidth),
  };
});

/**
 * 添加单位
 */
function addUnit(val) {
  return !isNaN(val) ? `${val}rpx` : val;
}

/**
 * 查询元素属性
 * @param queryText 查询的元素
 * @returns {Promise<unknown>}
 */
function queryRectProp() {
  let query = uni.createSelectorQuery().in(instance);
  return new Promise((resolve, reject) => {
    query
      .select("#hiddenText")
      .boundingClientRect((val) => {
        if (!val) {
          // 如果获取不到元素，等待一段时间后重试
          setTimeout(() => {
            queryRectProp().then(resolve).catch(reject);
          }, 100);
          return;
        }
        resolve(val.height);
      })
      .exec();
  });
}

/**
 * 设置隐藏文本
 * @param val
 * @returns {Promise<unknown>}
 */
function setHiddenText(val) {
  return new Promise((resolve, reject) => {
    data.content = val;
    nextTick(() => {
      resolve(val);
    });
  });
}

/**
 * 计算隐藏文本
 * @param maxHeight 最大高度
 */
function calcEllipsisText(maxHeight) {
  const tail = async (left, right) => {
    if (right - left <= 1) {
      return _props.text.slice(0, left) + data.dots;
    }
    const middle = Math.round((left + right) / 2);
    await setHiddenText(_props.text.slice(0, middle) + data.dots + data.suffixText);
    const height = await queryRectProp();
    if (parseInt(height) > parseInt(maxHeight)) {
      return tail(left, middle);
    }
    return tail(middle, right);
  };
  return tail(0, _props.text.length).then(res => {
    data.content = res;
    data.show = true;
  });
}

function onClickAction() {
  data.isOpen = !data.isOpen;
  data.suffixText = data.isOpen ? _props.suffixClose : _props.suffixOpen;
}
</script>

<style lang="scss" scoped>
.text-ellipsis-wrap {
  position: relative;
  z-index: 1;
}

.text-ellipsis {
  display: inline-block;
  word-break: break-all;
  overflow-wrap: break-word;
}

.hiddenText {
  position: fixed;
  top: -9999px;
  z-index: -999;
}
</style>
